import {
  docColor,
  DocumentObject,
  IAssemblyDocTreeNode,
  TaskManager,
} from '@amcax/base';
import { convertMatrix, RendererAssembly, RendererMgr } from '@amcax/renderer';
import { loadProgressEmit } from '@app-base/common/events/CoreEvent';
import { ctx2 } from '@app-cad/module';
import { chunk } from 'lodash';
import { Matrix4 } from 'three-legacy';
import { getPartsNumber } from './assembly-funcs';

export interface LoadRefDocOptions {
  initialRender?: boolean;
  noRender?: boolean;
}

const renderTaskManager = new TaskManager(50);

export const allParts = new Map<string, IAssemblyDocTreeNode>();

const partRefDocObjectsFetched = new Map<string, any>();
const refDocObjectsPromiseCache = new Map<string, Promise<any>>();

export const removeRefDocObjects = (partKey: string) => {
  partRefDocObjectsFetched.delete(partKey);
  refDocObjectsPromiseCache.delete(partKey);
};

const partShapeIdsFetched = new Map<string, string[][]>();
const refDocDataPromiseCache = new Map<string, Promise<void>>();

export async function getPartDocObjects({ assembly }: IAssemblyDocTreeNode) {
  const partKey = getPartRefKey(assembly);
  if (refDocObjectsPromiseCache.has(partKey)) {
    await refDocObjectsPromiseCache.get(partKey);
  }

  if (partRefDocObjectsFetched.has(partKey)) {
    return partRefDocObjectsFetched.get(partKey);
  }

  const refDocObjectsPromise = ctx2.commandCenters.assemblyCommands
    .getRefDocObjects(assembly.docid, assembly.docver)
    .then((subDocObjs) => {
      partRefDocObjectsFetched.set(partKey, subDocObjs);
      refDocObjectsPromiseCache.delete(partKey);

      return subDocObjs;
    });

  refDocObjectsPromiseCache.set(partKey, refDocObjectsPromise);

  return refDocObjectsPromise;
}

export function getVisualDataAndRenderPart(
  doc: IAssemblyDocTreeNode,
  { initialRender, noRender }: LoadRefDocOptions,
): Promise<void> {
  const { assembly, children } = doc;
  const partKey = getPartRefKey(assembly);

  async function fetchBatchesInSeries(batches: string[][], assembly: any) {
    const partRefKey = getPartRefKey(assembly);
    if (refDocDataPromiseCache.has(partKey)) {
      await refDocDataPromiseCache.get(partKey);
    }

    let shapeIdsBatches = partShapeIdsFetched.get(partRefKey);
    if (shapeIdsBatches) {
      shapeIdsBatches.forEach((shapeIds) =>
        renderTaskManager.add(async () => {
          if (!noRender && doc.recursivelyVisible && doc.visible) {
            renderPart(doc, shapeIds, initialRender);
          }
        }),
      );

      return;
    }

    shapeIdsBatches = [];
    const batchSize = batches.length;
    let currentBatchNumber = 0;
    await Promise.all(
      batches.map(async (batch) => {
        const data =
          await ctx2.commandCenters.assemblyCommands.getRefDocVisualData(
            assembly.docid,
            assembly.docver,
            batch,
          );
        const renderer = RendererMgr.getRender<RendererAssembly>('assembly');
        renderer.saveShapeData(data, partKey);

        const shapeIds = data.map(({ id }) => id);
        shapeIdsBatches.push(shapeIds);

        if (!noRender && doc.recursivelyVisible && doc.visible) {
          renderPart(doc, shapeIds, initialRender);
        }

        const { currentPartsNumber, totalPartsNumber } = getPartsNumber();
        if (totalPartsNumber) {
          loadProgressEmit(
            ((currentPartsNumber + currentBatchNumber++ / batchSize) /
              totalPartsNumber) *
              100,
          );
        }
      }),
    );

    partShapeIdsFetched.set(partRefKey, shapeIdsBatches);

    return;
  }

  function getLeafKeys(obj: IAssemblyDocTreeNode): string[] {
    if (!obj.visible) {
      return [];
    }

    if (!obj.children || obj.children.length === 0) {
      return [obj.key.split(':').at(-1)];
    }

    return obj.children.flatMap(getLeafKeys);
  }

  const dxidList = children.flatMap((obj: any) => getLeafKeys(obj));

  const batches = chunk<string>(dxidList, 50);

  // fetch visual data and add shapes, then return the shape ids
  const visualDataPromise = fetchBatchesInSeries(batches, assembly);

  visualDataPromise.then((shapeIds) => {
    refDocDataPromiseCache.delete(partKey);
    return shapeIds;
  });

  refDocDataPromiseCache.set(partKey, visualDataPromise);
  return visualDataPromise;
}

export function addPartToDataManager(
  part: IAssemblyDocTreeNode,
  partKey: string,
) {
  const cloneOriginalObj = (
    obj: DocumentObject,
    prefix: string,
  ): DocumentObject => {
    const clonedObj = { ...obj, dxid: `${prefix}:${obj.dxid}`, partKey };
    ctx2.dataManager.addObjectsInfo(clonedObj);
    if (clonedObj.children) {
      clonedObj.children = clonedObj.children.map((child) =>
        cloneOriginalObj(child, prefix),
      );
    }
    return clonedObj;
  };
  const clonedOriginalObjects =
    part.children?.map((child) =>
      cloneOriginalObj(child.originalObj as DocumentObject, part.key),
    ) || [];

  const getMergedColor = (node: IAssemblyDocTreeNode): docColor => {
    const merged: docColor = {};
    const chain: IAssemblyDocTreeNode[] = [];

    // Collect all ancestors
    let current: IAssemblyDocTreeNode | undefined = node;
    while (current) {
      chain.push(current);
      current = current.parent;
    }

    // Merge from the root ancestor down; child's keys overwrite parent
    chain.reverse().forEach((ancestor) => {
      if (ancestor.color) {
        for (const [key, value] of Object.entries(ancestor.color)) {
          merged[key] = { ...merged[key], ...value };
        }
      }
    });

    return merged;
  };

  addObjectsInfoRecursively(
    clonedOriginalObjects,
    part.recursivelyVisible,
    getMergedColor(part),
  );
}

function addObjectsInfoRecursively(
  objects: DocumentObject[],
  parentRecursivelyVisible: boolean,
  color: docColor,
) {
  objects.forEach((object) => {
    const isRecursiveVisible = parentRecursivelyVisible && object.visibility;

    ctx2.dataManager.addObjectsInfo({
      ...object,
      isRecursiveVisible,
      color: { ...object.color, ...color },
    });

    if (object.children) {
      addObjectsInfoRecursively(object.children, isRecursiveVisible, color);
    }
  });
}

export function renderPart(
  part: IAssemblyDocTreeNode,
  ids: string[] = [],
  initialRender = false,
) {
  const renderer = RendererMgr.getRender<RendererAssembly>('assembly');

  renderer
    .drawNewModels(
      ids.map((id) => `${part.key}:${id}`),
      getPartRefKey(part.assembly),
    )
    .then(() => {
      renderer.transformPart(part, ids);

      if (initialRender) {
        ctx2.viewer.fitObjectView();
      }
    });
}

export function partMapper(
  docObj: DocumentObject,
  parent: IAssemblyDocTreeNode,
  partKey: string,
  params: any,
): IAssemblyDocTreeNode {
  const getVisibility = (dxid, visible, params) => {
    if (params?.show?.includes(dxid)) {
      return true;
    }
    if (params?.hide?.includes(dxid)) {
      return false;
    }
    return visible;
  };

  const visible = getVisibility(docObj.dxid, docObj.visibility, params);

  const node = {
    key: `${partKey}:${docObj.dxid}`,
    originalObj: docObj,
    title: docObj.label,
    type: docObj.type,
    visible: visible,
    color: docObj.color,
    recursivelyVisible: parent.recursivelyVisible ?? visible,
    parent,
    children: [],
  };

  if (docObj.children) {
    node.children = docObj.children.map((child) =>
      partMapper(child, node, partKey, params),
    );
  }

  return node;
}

export function collectTransformationMatrices(node: IAssemblyDocTreeNode) {
  const result: Matrix4[] = [];
  let currentNode = node;

  while (currentNode) {
    if (currentNode.assembly?.matrix) {
      result.push(convertMatrix(currentNode.assembly?.matrix));
    }
    currentNode = currentNode.parent;
  }

  return result;
}

export function getPartRefKey(
  assembly: DocumentObject['assembly'],
): string | undefined {
  return assembly && `${assembly.docid}@${assembly.docver}`;
}

export function findSubModel(node: IAssemblyDocTreeNode, modelDxid: string) {
  if ((node.originalObj as DocumentObject)?.dxid === modelDxid) {
    return node;
  }
  for (const child of node.children) {
    const result = findSubModel(child, modelDxid);
    if (result) {
      return result;
    }
  }
  return undefined;
}

export function getProductPartSubModel(key: string) {
  if (!key) {
    return null;
  }
  const [partKey, subId] = key.split(':');
  const part = allParts.get(partKey);

  if (!part) {
    return null;
  }

  return findSubModel(part, subId);
}

export const getProductDocumentObject = (key: string) => {
  return getProductPartSubModel(key)?.originalObj ?? null;
};
